{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit RadioStations;

interface

uses Classes, RadioTuner, AHMTypes;

const
  FREQ_MULTIPLIER = 1000; // Frequency multiplier to convert XML values to MHz
  FREQ_NAME_SEP = '.'; // Frequency name separator character
  RADIO_FILENAME = 'RadioStations.xml';

//TODO - these default names need to be translated in theme
  DEFAULT_NAME = 'Preset'; // Default name for radio presets if no RDS name
  DEFAULT_GENRE = 'General'; // Default name for radio genre if no RDS PTY

  UPDATE_RDS_INTERVAL = 100; // Duration in milliseconds between RDS updates
  FM_FREQ_DUMMY = 10000; // Invalid FM frequency for dummy station preset
  FM_FREQ_MIN = 87500; // UK/USA/AU FM Tuning frequency range (minimum)
  FM_FREQ_MAX = 108000; // UK/USA/AU FM Tuning frequency range (maximum)
  FM_FREQ_RANGE = FM_FREQ_MAX - FM_FREQ_MIN;

  META_FREQ_MAX = FM_FREQ_RANGE div TUNING_INTERVAL; // No. of tuning steps

type
  TAHMRadioStation = class
  private
    FName: String;
    FGenre: String;
    FFrequency: LongInt;
  protected
    function GetGenre: String;
    function GetFileName: String;
    function GetDisplayName: String;
  public
    property Name: String read FName write FName;
    property Genre: String read GetGenre write FGenre;
    property Frequency: LongInt read FFrequency write FFrequency;
    property Filename: String read GetFileName;
    property Displayname: String read GetDisplayName;
    function Equals(Name: String; Frequency: LongInt): Boolean;
    procedure SaveAsXML(Lines: TStrings);
  end;

  TAHMRadioStationList = class
  private
    FList: TList;
    FCurrent: LongInt;
  protected
    function GetCount: Integer;
    function GetStation(Index: Integer): TAHMRadioStation;
  public
    constructor Create;
    destructor Destroy; override;
    property Count: Integer read GetCount;
    property CurrentFrequency: LongInt read FCurrent write FCurrent;
    property Stations[Index: Integer]: TAHMRadioStation read GetStation; default;
    function LookupStation(Filename: String): TAHMRadioStation;
    function AddStation(Name, Genre: String; Frequency: LongInt): TAHMRadioStation;
    function DeleteStation(Name: String; Frequency: LongInt): Boolean;
    procedure ListGenres(Lines: TStringList);
    procedure Clear;
    procedure SaveAsXML(Lines: TStrings);
  end;

  TAHMRadioStationManager = class
  private
    FGenres: TStringList;
    FTuner: TAHMRadioTuner;
    FStations: TAHMRadioStationList;
    FLastUpdated: TAHMTimeStamp;
    FRetuned: Boolean;
  protected
    function ParseXML(const Filepath: String): Boolean;
    procedure LogParsingError(const sErrorCode: String);
  public
    constructor Create;
    destructor Destroy; override;
    property Stations: TAHMRadioStationList read FStations;
    property Genres: TStringList read FGenres;
    procedure LoadRadioSettings;
    procedure SaveRadioSettings;
    function TuneRadio(Filename: String): Boolean;
    function SeekUp: Boolean;
    function SeekDown: Boolean;
    procedure AddPreset;
    procedure DeletePreset;
    procedure UpdateMetaData;
  end;

implementation

uses SysUtils, ApplicationManager, TagNames, XMLTags, XMLHelper, AHMConsts,
     MetaConsts, ErrorCodes, Logging, Timing;

//----------------------------------------------------------------------------//

function TAHMRadioStation.GetGenre: String;
begin
  // Clean up formatting of genre name
  Result := LowerCase(Trim(FGenre));
  if Length(Result) > 0 then Result[1] := UpCase(Result[1]);
end;

function TAHMRadioStation.GetFileName: String;
begin
  // Generate a dummy filename for this radio station
  Result := LowerCase(Trim(FName)) + FREQ_NAME_SEP + IntToStr(FFrequency);
end;

function TAHMRadioStation.GetDisplayName: String;
begin
  // Concatenate frequency in MHz and name for this radio station
  if FFrequency = FM_FREQ_DUMMY then
    Result := Float2Str(FM_FREQ_MIN / FREQ_MULTIPLIER) + ' ' + Trim(FName)
  else
    Result := Float2Str(FFrequency / FREQ_MULTIPLIER) + ' ' + Trim(FName);
end;

function TAHMRadioStation.Equals(Name: String; Frequency: LongInt): Boolean;
var
  Me: String;
begin
  // Assume stations are the same if frequency or name matches, although
  // we don't consider two stations with the default name to be a match
  // unless the frequencies are also the same
  Result := (Frequency <> 0) and (FFrequency = Frequency);

  if (not Result) and (FName <> DEFAULT_NAME) then
  begin
    Me := LowerCase(Trim(FName));
    Result := (Me <> '') and (Me = LowerCase(Trim(Name)));
  end;
end;

procedure TAHMRadioStation.SaveAsXML(Lines: TStrings);
begin
  Lines.Add(TABS[2] + TGB+TAG_STATION_PRESET+SPC+ATTR_NAME+ATB +
            EncodeXML(FName) + ATN+ATTR_GENRE+ATB + EncodeXML(FGenre) +
            ATC + Float2Str(FFrequency / FREQ_MULTIPLIER) + TGE +
            TAG_STATION_PRESET + TGC);
end;

//----------------------------------------------------------------------------//

constructor TAHMRadioStationList.Create;
begin
  inherited Create;

  FList := TList.Create;
end;

destructor TAHMRadioStationList.Destroy;
begin
  Clear;
  FList.Free;

  inherited Destroy;
end;

function TAHMRadioStationList.GetCount: Integer;
begin
  Result := FList.Count;
end;

function TAHMRadioStationList.GetStation(Index: Integer): TAHMRadioStation;
begin
  if (Index >= 0) and (Index < FList.Count) then
    Result := TAHMRadioStation(FList.Items[Index])
  else
    Result := nil;
end;

function TAHMRadioStationList.LookupStation(Filename: String): TAHMRadioStation;
var
  i: Integer;
  Name: String;
  Frequency: LongInt;
begin
  // Default result - not found
  Result := nil;
  Name := '';
  Frequency := 0;

  // Parse filename - look for separator char
  if Length(Filename) > 0 then
    for i := Length(Filename) downto 1 do
      if Filename[i] = FREQ_NAME_SEP then
      begin
        Name := LowerCase(Trim(Copy(Filename, 1, Pred(i))));
        Frequency := StrToIntDef(Copy(Filename, Succ(i), Length(Filename)), 0);
        Break;
      end;

  // If parsed okay then look for matching station
  if (Length(Name) > 0) and (Frequency > 0) then
    for i := 0 to Pred(FList.Count) do
      if TAHMRadioStation(FList[i]).Equals(Name, Frequency) then
      begin
        Result := TAHMRadioStation(FList[i]);
        Exit;
      end;
end;

function TAHMRadioStationList.AddStation(Name, Genre: String; Frequency: LongInt): TAHMRadioStation;
var
  i: Integer;
begin
  // Check for existing stations with same name and frequency (exclude dummy)
  for i := 1 to Pred(FList.Count) do
    if TAHMRadioStation(FList[i]).Equals(Name, Frequency) then
    begin
      // Just update our existing station instead
      Result := TAHMRadioStation(FList[i]);
      Result.Genre := Genre;
      Result.Name := Name;
      Exit;
    end;

  // This is a unique station, so add it
  Result := TAHMRadioStation.Create;
  Result.Name := Name;
  Result.Genre := Genre;
  Result.Frequency := Frequency;
  FList.Add(Result);
end;

function TAHMRadioStationList.DeleteStation(Name: String; Frequency: LongInt): Boolean;
var
  i: Integer;
begin
  // Set default result - not found/deleted
  Result := False;

  // Check for existing stations with same name and frequency (exclude dummy)
  for i := 1 to Pred(FList.Count) do
    if TAHMRadioStation(FList[i]).Equals(Name, Frequency) then
    begin
      TAHMRadioStation(FList[i]).Free;
      FList.Delete(i);

      // Success - deleted preset
      Result := True;
      Exit;
    end;
end;

procedure TAHMRadioStationList.ListGenres(Lines: TStringList);
var
  i: Integer;
begin
  if Assigned(Lines) then
  begin
    // Ensure the list will be de-duped
    Lines.Clear;
    Lines.Sorted := True;
    Lines.Duplicates := dupIgnore;
    Lines.CaseSensitive := False;

    // Add each genre we have available (exclude dummy)
    for i := 1 to Pred(FList.Count) do
      Lines.Add(TAHMRadioStation(FList[i]).Genre);
  end;
end;

procedure TAHMRadioStationList.Clear;
var
  i: Integer;
begin
  for i := 0 to Pred(FList.Count) do
    TAHMRadioStation(FList.Items[i]).Free;
  FList.Clear;
end;

procedure TAHMRadioStationList.SaveAsXML(Lines: TStrings);
var
  i: Integer;
begin
  // Add current frequency
  Lines.Add(TABS[1] + BeginTag(TAG_CURRENT_STATION) +
            Float2Str(FCurrent / FREQ_MULTIPLIER) +
            EndTag(TAG_CURRENT_STATION));

  // Add a list of preset stations (exclude dummy)
  Lines.Add(TABS[1] + BeginTag(TAG_STATION_PRESETS));

  for i := 1 to Pred(FList.Count) do
    TAHMRadioStation(FList[i]).SaveAsXML(Lines);

  Lines.Add(TABS[1] + EndTag(TAG_STATION_PRESETS));
end;

//----------------------------------------------------------------------------//

constructor TAHMRadioStationManager.Create;
begin
  inherited;

  FStations := TAHMRadioStationList.Create;
  FTuner := TAHMUSBRadioDLLTuner.Create;
  FGenres := TStringList.Create;
end;

destructor TAHMRadioStationManager.Destroy;
begin
  FStations.Free;
  FTuner.Free;
  FGenres.Free;

  inherited;
end;

procedure TAHMRadioStationManager.LoadRadioSettings;
var
  Settings: TStringList;
begin
  // First clear existing stations
  FStations.Clear;

  // Add a dummy radio station in case none are configured
  FStations.AddStation(DEFAULT_NAME, '', FM_FREQ_DUMMY);

  // Parse XML to populate custom state values
  ParseXML(ApplicationManagerInstance.ConfigPath);
  FStations.ListGenres(FGenres);

  // Log our state settings for support purposes
  Settings := TStringList.Create;
  try
    Settings.Add('mediate radio settings are as follows:');
    FStations.SaveAsXML(Settings);
    LogInfo(Settings);
  finally
    Settings.Free;
  end;
end;

procedure TAHMRadioStationManager.SaveRadioSettings;
var
  Settings: TStringList;
begin
  Settings := TStringList.Create;
  try
    Settings.Add(XML_PREAMBLE);
    Settings.Add(BeginTag(TAG_RADIO_CONFIG));
    FStations.SaveAsXML(Settings);
    Settings.Add(EndTag(TAG_RADIO_CONFIG));

    Settings.SaveToFile(ApplicationManagerInstance.ConfigPath + RADIO_FILENAME);
  finally
    Settings.Free;
  end;
end;

function TAHMRadioStationManager.TuneRadio(Filename: String): Boolean;
var
  Station: TAHMRadioStation;
begin
  // Set default result - failed to tune
  Result := False;

  // Locate appropriate station for specified filename
  Station := FStations.LookupStation(Filename);

  if Assigned(Station) and FTuner.Ready then
  begin
    FTuner.Enabled := True;
    FTuner.Frequency := Station.Frequency;
    FStations.CurrentFrequency := Station.Frequency;

    // Success - tuned to new frequency
    Result := True;
    FRetuned := True;
  end;
end;

function TAHMRadioStationManager.SeekUp: Boolean;
begin
  // Set default result - failed to seek
  Result := False;

  if FTuner.Enabled then
  begin
    FTuner.SeekUp;
    FStations.CurrentFrequency := FTuner.Frequency;
    Result := True;
    FRetuned := True;
  end;
end;

function TAHMRadioStationManager.SeekDown: Boolean;
begin
  // Set default result - failed to seek
  Result := False;

  if FTuner.Enabled then
  begin
    FTuner.SeekDown;
    FStations.CurrentFrequency := FTuner.Frequency;
    Result := True;
    FRetuned := True;
  end;
end;

procedure TAHMRadioStationManager.AddPreset;
var
  Name, Genre: String;
begin
  if FTuner.Enabled then
  begin
    // Build sensible name from RDS if possible
    Name := FTuner.RDSStation;
    if Name = '' then Name := DEFAULT_NAME;
    Genre := FTuner.RDSProgramType;
    if Genre = '' then Genre := DEFAULT_GENRE;

    // Add current station to preset list
    FStations.AddStation(Name, Genre, FTuner.Frequency);
    FStations.ListGenres(FGenres);
  end;
end;

procedure TAHMRadioStationManager.DeletePreset;
begin
  if FTuner.Enabled then
  begin
    FStations.DeleteStation(FTuner.RDSStation, FTuner.Frequency);
    FStations.ListGenres(FGenres);
  end;
end;

function MetaFreqToFreq(MetaValue: Integer): LongInt;
begin
  Result := (MetaValue * TUNING_INTERVAL) + FM_FREQ_MIN;
end;

function FreqToMetaFreq(Frequency: LongInt): Integer;
begin
  Result := (Frequency - FM_FREQ_MIN) div TUNING_INTERVAL;
end;

procedure TAHMRadioStationManager.UpdateMetaData;
var
  Frequency: LongInt;
begin
  // Initialise tuner if not already available
  if FTuner.Ready and not FTuner.Enabled then
  begin
    FTuner.Enabled := True;
    FTuner.Frequency := FStations.CurrentFrequency;
    FRetuned := True;
  end;

  // Don't set metadata if tuner isn't initialised
  if not FTuner.Enabled then Exit;

  // Update every 100ms or so to avoid bogging CPU
  if TimerInstance.HasDurationElapsed(FLastUpdated, UPDATE_RDS_INTERVAL) then
  begin
    FLastUpdated := TimerInstance.CurrentTimeStamp;

    with ApplicationManagerInstance.MetaDataManager do
    begin
      // Check for external changes to frequency - if so then retune
      if not FRetuned then
      begin
        Frequency := MetaFreqToFreq(GetMetaInteger(META_RADIO_FREQ_VAL));
        if Frequency <> FTuner.Frequency then
        begin
          FTuner.Frequency := Frequency;
          FStations.CurrentFrequency := Frequency;
        end;
      end;

      // Update radio metadata settings
      SetMetaBoolean(META_RADIO_STEREO, FTuner.IsStereo);
      SetMetaInteger(META_RADIO_SIGNAL, FTuner.SignalStrength);
      SetMetaInteger(META_RADIO_SIGNAL_MAX, MAX_SIGNAL_STRENGTH);
      SetMetaValue(META_RADIO_PROGRAM, FTuner.RDSText);
      SetMetaValue(META_RADIO_STATION, FTuner.RDSStation);
      SetMetaValue(META_RADIO_REGION, FTuner.RDSRegion);
      SetMetaValue(META_RADIO_COUNTRY, FTuner.RDSCountry);
      SetMetaValue(META_RADIO_GENRE, FTuner.RDSProgramType);
      SetMetaValue(META_RADIO_FREQUENCY, Float2Str(FTuner.Frequency / FREQ_MULTIPLIER));
      SetMetaInteger(META_RADIO_FREQ_VAL, FreqToMetaFreq(FTuner.Frequency));
      SetMetaInteger(META_RADIO_FREQ_MAX, META_FREQ_MAX);

      // Okay, we've updated metadata so can clear retuned flag
      FRetuned := False;
    end;
  end;
end;

function TAHMRadioStationManager.ParseXML(const Filepath: String): Boolean;
var
  RootNode, ParseNode, StationNode: TAHMXMLNode;
  sName, sGenre: String;
  iFreq: LongInt;
begin
  // Default result - problem radio stations configuration
  Result := False;

  // Don't bother trying to parse XML file if its missing
  if not FileExists(FilePath + RADIO_FILENAME) then Exit;

  with TAHMXMLHelper.Create(Filepath + RADIO_FILENAME) do
  try
    // Ignore preamble - we're looking for a RadioConfiguration node
    if not FindRootElement(RootNode, TAG_RADIO_CONFIG) then
      LogParsingError(XML_PRS_RADIO_MISSING_START_TAG);

    // What nodes do we have here?
    ParseNode := nil;
    while NextElement(RootNode, ParseNode) do
    begin
      if CurrentElement = TAG_CURRENT_STATION then
      begin
        iFreq := Round(StrToFloatDef(GetValue, 0) * FREQ_MULTIPLIER);
        if iFreq > 0 then FStations.CurrentFrequency := iFreq;
      end
      else if CurrentElement = TAG_STATION_PRESETS then
      begin
        StationNode := nil;
        while FindElement(ParseNode, StationNode, TAG_STATION_PRESET) do
        begin
          sName := GetAttribute(ATTR_NAME);
          sGenre := GetAttribute(ATTR_GENRE);
          iFreq := Round(StrToFloatDef(GetValue, 0) * FREQ_MULTIPLIER);
          if (sName <> '') and (iFreq > 0) then
            FStations.AddStation(sName, sGenre, iFreq);
        end;
      end
      else
        LogParsingError(XML_PRS_RADIO_FOUND_UNKNOWN_CONTENT_TAG);
    end;

    // All is well, parsed radio stations file successfully
    Result := True;
  finally
    Free;
  end;
end;

procedure TAHMRadioStationManager.LogParsingError(const sErrorCode: String);
begin
  // Log error silently but there's no need to throw an exception
  LogWarn('Encountered error whilst parsing RadioStation XML: ' + sErrorCode);
end;

end.
